//
// JSDownloader.cpp : Defines the entry point for the console application.
//

#if defined(_MSC_VER)
// Specifies that the minimum required platform is Windows XP.
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0501
#endif

#define _CRT_SECURE_NO_WARNINGS
#endif

#include <stdio.h>
#include <malloc.h>
#include <string.h>
#include <iostream>
#include <sstream>
#include "v8/v8.h"

#define CURL_STATICLIB
#include "curl/curl.h"
#include <sys/stat.h>
#include <sys/types.h>

#include "rapidxml-1.13/rapidxml.hpp"
#include "rapidxml-1.13/rapidxml_utils.hpp"
#include "rapidxml-1.13/rapidxml_print.hpp"

using namespace std;
using namespace v8;
using namespace rapidxml;

#define SEP_INDICATOR_CHAR ','
#define SEP_INDICATOR_STR ","

#define PAGE_CACHE_SIZE 512*1024
#define MAX_REDIRECT_NUM  256

#define USE_CURL_API_DOWNLOAD

typedef struct WebPage {
  char *data;
  size_t size;
  size_t left;
}WebPage;

#if defined(_MSC_VER)
#pragma comment(lib,"Winmm.lib")
#pragma comment(lib,"Ws2_32.lib")
#pragma comment(lib,"v8_base.lib")
#pragma comment(lib,"v8_snapshot.lib")

#pragma comment(lib,"libmingwex.a")
#pragma comment(lib,"libgcc.a")
#pragma comment(lib,"libwldap32.a")
#pragma comment(lib,"libssl.a")
#pragma comment(lib,"libcrypto.a")
#pragma comment(lib,"librtmp.a")
#pragma comment(lib,"libz.a")
#pragma comment(lib,"libcurl.a")

// MAGIC code for the fuck-needed linker of Visual C++
// just a stub to force CRT to link _fstat64,...
// MUST not be static function
int fuck_fstati64( int fd, struct _stat64 *buffer)
{
  // Note: we can not use _fstati64() because it is a marcro of _fstat64()
  return _fstat64(fd, buffer);
}

int fuck_stati64(const char * name, struct _stat64 * stat)
{
  return _stat64(name, stat);
}
#endif

//TODO: use smart_ptr

// we use IE's user-agent, some websites(such as http://v.sohu.com) refuse 3rd user-agent
#if 0
static const char g_user_agent[]={"curl/7.21.7 (i686-pc-mingw32) libcurl/7.21.7 OpenSSL/1.0.0d zlib/1.2.5 librtmp/2.3"};
#else
static const char g_user_agent[]={"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 1.1.4322; .NET4.0C; .NET4.0E)"};
#endif

typedef size_t (*CURL_WRITE_CALLBACK)(const void *, size_t, size_t, void *);

static size_t curl_write_mem(const void *ptr, size_t size, size_t count, void *stream)
{
  WebPage *page = (WebPage*)stream;

  size *= count;
  if (!page || size <= 0)
    return 0;
  // +1 for the termination: NULL
  if (page->left < size+1) {
    char *newbuf = (char*)realloc(page->data, page->size+page->left+size+PAGE_CACHE_SIZE);
    if (!newbuf)
      return 0;
    page->data = newbuf;
    page->left += size+PAGE_CACHE_SIZE;
  }
  memcpy(page->data + page->size, ptr, size);
  page->left -= size;
  page->size += size;
  // Terminated with NULL
  page->data[page->size] = 0;

  return size;
}

static size_t curl_write_file(const void *ptr, size_t size, size_t count, void *stream)
{
  if (size <= 0 || !stream)
    return 0;
  fwrite(ptr, size, count, (FILE*)stream);

  return size * count;
}

static int curl_download(CURL_WRITE_CALLBACK callback, void *opaque, char *url, int method, void *param, int progress)
{
  char error_buf[CURL_ERROR_SIZE+1];
  CURL *curl = curl_easy_init();

  if (!curl)
    return -1;

#ifdef _DEBUG
  curl_easy_setopt(curl,CURLOPT_VERBOSE, 1);
#endif

  curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error_buf);
  curl_easy_setopt(curl, CURLOPT_URL, url);
  // Data receiver
  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, callback);
  curl_easy_setopt(curl, CURLOPT_WRITEDATA, opaque);
  if (method) {
    curl_easy_setopt(curl, CURLOPT_POST, 1);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, param);
  }
  // We emulate the curl App's default progress msg
  if (progress) {
    curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
    curl_easy_setopt(curl, CURLOPT_STDERR, NULL);
  }
  // Avoid dead-loop: we limit the MAX redir num to |MAX_REDIRECT_NUM|
  curl_easy_setopt(curl, CURLOPT_MAXREDIRS, MAX_REDIRECT_NUM);
  // MUST set this option to follow 302, 301
  curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
  // Explicitly announce we support "deflate, gzip"
  curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
  // MUST setup user-agent as same as curl standlone application,
  // otherwise, some web servers(eg: http://www.tudou.com)
  // will refuse curl application's HTTP request
  curl_easy_setopt(curl, CURLOPT_USERAGENT, g_user_agent);
  CURLcode ret = curl_easy_perform(curl);
  if (ret != CURLE_OK) {
    curl_easy_cleanup(curl);
    return -1;
  }
  curl_easy_cleanup(curl);

  return 0;
}

static Handle<Value> ku6_download_page(const Arguments &args)
{
  HandleScope handle_scope;

  if (args.Length() == 1 && args[0]->IsString()) {
    WebPage page;
    String::AsciiValue url(args[0]->ToString());

    memset(&page, 0, sizeof(WebPage));
    if (curl_download(curl_write_mem, &page, *url, 0, NULL, 0) < 0) {
      free(page.data);
      return Undefined();
    }
    Handle<String> hString = String::New(page.data, page.size);
    free(page.data);
    return hString;
  }

  return Undefined();
}

static Handle<Value> ku6_download_page_post(const Arguments &args)
{
  HandleScope handle_scope;

  if (args.Length() == 2 && args[0]->IsString()) {
    WebPage page;
    String::AsciiValue url(args[0]->ToString());
    String::AsciiValue param(args[1]->ToString());

    memset(&page, 0, sizeof(WebPage));
    if (curl_download(curl_write_mem, &page, *url, 1, *param, 0) < 0) {
      free(page.data);
      return Undefined();
    }
    Handle<String> hString = String::New(page.data, page.size);
    free(page.data);
    return hString;
  }

  return Undefined();
}

static int rapidxml_parse_xml(const Arguments &args, int argc, int bAttribute, ostringstream &os)
{
  int i, bFind = 0;

  HandleScope handle_scope;
  String::AsciiValue xml(args[0]->ToString());
  xml_document<>  doc;
  doc.parse<0>(*xml);
  xml_node<>* root = doc.first_node();
  if (!root)
    return -1;

  argc--;
  String::AsciiValue **pKeyStub = new String::AsciiValue*[argc];
  if (!pKeyStub)
    return -1;
  memset(pKeyStub, 0, sizeof(String::AsciiValue *) * argc);

  for (i= 0; i<argc; i++) {
    pKeyStub[i] = new String::AsciiValue(args[i+1]->ToString());
    if (!pKeyStub[i])
      break;
  }

  if (i != argc) {
    for (i=0; i<argc; i++)
      delete pKeyStub[i];
    delete pKeyStub;
    return -1;
  }

  int argc_key = argc;
  // The last argument is |xml_attribute|
  String::AsciiValue attribute_key(args[argc]->ToString());
  if (bAttribute)
    argc_key--;//We skip the last key which is |xml_attribute|

  xml_node<>* node = root->first_node(**pKeyStub[0]);
  for (; node; node = node->next_sibling(**pKeyStub[0])) {
    xml_node<>* sub_node = node;
    for (i=1; i<argc_key; i++) {
      sub_node = sub_node->first_node(**pKeyStub[i]);
      if (!sub_node)
        break;
    }

    if (bFind)
      os << SEP_INDICATOR_STR;
    if (i >= argc_key && sub_node) {
      if (bAttribute) {
        xml_attribute<> *attr_node = sub_node->first_attribute();
        for (; attr_node; attr_node = attr_node->next_attribute()) {
          char *pAttribute = attr_node->name();
          if (pAttribute && !strcmp(pAttribute, *attribute_key)) {
            os << attr_node->value();
            bFind=1;
            break;
          }
        }
      } else {
        xml_node<>* child = sub_node->first_node();
        if (child)
          os << child->value();
        else
          os << sub_node->value();
        bFind=1;
      }
    }
  }

  for (i=0; i<argc; i++)
    delete pKeyStub[i];
  delete pKeyStub;

  return 0;
}

static Handle<Value> ku6_parse_xml(const Arguments &args)
{
  int argc = args.Length();

  if (argc < 2)
    return Undefined();

  HandleScope handle_scope;
  ostringstream os;
  if (rapidxml_parse_xml(args, argc, 0, os) < 0)
    return Undefined();
  Handle<String> hString = String::New(os.str().c_str(), os.str().size());

  return hString;
}

static Handle<Value> ku6_parse_xml_attribute(const Arguments &args)
{
  int argc = args.Length();

  if (argc < 3)
    return Undefined();

  HandleScope handle_scope;
  ostringstream os;
  if (rapidxml_parse_xml(args, argc, 1, os) < 0)
    return Undefined();
  Handle<String> hString = String::New(os.str().c_str(), os.str().size());

  return hString;
}

static Handle<Value> ku6_copyBetween(const Arguments &args)
{
  if (args.Length() != 3)
    return Undefined();

  HandleScope handle_scope;
  String::AsciiValue input(args[0]->ToString());
  String::AsciiValue start(args[1]->ToString());
  String::AsciiValue end(args[2]->ToString());
  char *ptr = strstr(*input, *start);
  if (ptr) {
    ptr += start.length();
    char *ptr2 = strstr(ptr, *end);
    Handle<String> hString;
    if (ptr2)
      hString = String::New(ptr, ptr2-ptr);
    else
      hString = String::New(ptr);// We assume the whole string is qualified
    return hString;
  }

  return Undefined();
}

//Note: we assume |src| is enough to hold all the replacement of |strNew|
static char* strReplace(char *src, int maxLen, const char *strOld, const char *strNew, int count)
{
  char *pTemp = src;
  int iLength, iDiff;

  if (src == NULL || strOld == NULL || strNew == NULL)
    return NULL;

  iLength = strlen(strOld);
  iDiff = iLength - strlen(strNew);

  if (iLength == 0)
   return src;

  while ((pTemp = strstr(pTemp, strOld)) && count > 0) {
    const char *p = strNew;
    if (iDiff < 0) {
      int i = strlen(src);
      if (i-iDiff >= maxLen)
        return NULL;
      while (i >= pTemp-src+iLength) {
        src[i-iDiff] = src[i];
        i--;
      }
    }
    while (*p != '\0')
      *pTemp++ = *p++;

    if(iDiff > 0)
      strcpy(pTemp, pTemp+iDiff);
    count--;
  }

  return src;
}

static Handle<Value> ku6_strReplace(const Arguments &args)
{
  if (args.Length() != 3)
    return Undefined();

  HandleScope handle_scope;
  String::AsciiValue input(args[0]->ToString());
  String::AsciiValue strOld(args[1]->ToString());
  String::AsciiValue strNew(args[2]->ToString());

  if (!*input || !*strOld || !*strNew)
    return Undefined();

  int len = strlen(*input);
  int cache_size = len + PAGE_CACHE_SIZE;
  // Avoid overflow
  if (cache_size < len)
    return Undefined();
  char *cache = new char[cache_size];
  if (!cache)
    return Undefined();
  strcpy(cache, *input);

  char *ptr = strReplace(cache, cache_size, *strOld, *strNew, 0x7FFFFFFF);
  if (ptr) {
    Handle<String> hString = String::New(ptr);
    delete[] cache;
    return hString;
  }
  delete[] cache;

  return Undefined();
}

static Handle<Value> ku6_strFormat(const Arguments &args)
{
  int i, argc = args.Length();

  if (argc < 2)
    return Undefined();

  HandleScope handle_scope;
  String::AsciiValue input(args[0]->ToString());
  if (!*input)
    return Undefined();

  int len = strlen(*input);
  int cache_size = len + PAGE_CACHE_SIZE;
  if (cache_size < len)
    return Undefined();
  char *cache = new char[cache_size];
  if (!cache)
    return Undefined();
  strcpy(cache, *input);

  char key[16], *ptr = cache;
  for (i=1; i<argc; i++) {
    sprintf(key, "%%%d", i);
    String::AsciiValue replace(args[i]->ToString());
    char *ptrMatch = strstr(ptr, key);
    if (ptrMatch) {
      len = cache_size - (ptr - cache);
      if (!strReplace(ptr, len, key, *replace, 0x7FFFFFFF)) {
        delete[] cache;
        return Undefined();
      }
      ptr += strlen(*replace);
    }
  }
  Handle<String> hString = String::New(cache);
  delete[] cache;

  return hString;
}

static int hexchar2int(char c)
{
  if (c >= '0' && c <= '9') return c - '0';
  if (c >= 'a' && c <= 'f') return c - 'a' + 10;
  if (c >= 'A' && c <= 'F') return c - 'A' + 10;
  return -1;
}

static char * decodeURL(char *URL)
{
  if (!URL)
    return NULL;

  char *p = strchr(URL, '%');

  // Replace hex chars to ascii chars
  while (p) {
    char strOld[4];
    char strNew[2];
    unsigned int v = hexchar2int(p[1]);

    v <<= 4;
    v |= hexchar2int(p[2]);

    strNew[0] = v;
    strNew[1] = 0;

    strOld[0] = p[0];
    strOld[1] = p[1];
    strOld[2] = p[2];
    strOld[3] = 0;

    strReplace(p, 4, strOld, strNew, 1);
    // MUST be |p++|, because |strNew| could be '%'
    p++;
    p = strchr(p,'%');
  }

  return URL;
}

static char * cleanURL(char *URL)
{
  char *p0 = strchr(URL, '?');
  char *p = strchr(URL, '%');
  if (p0 && p) {
    // we only modify the unique URL 'http:%3Axxx%2Fyyyy?zzzz'
    // we do not accept any URL with parameters '%' after '?'
    if (p0 < p)
      return URL;
  }

  decodeURL(p);

  return URL;
}

static Handle<Value> ku6_clean_url(const Arguments &args)
{
  if (args.Length() != 1)
    return Undefined();

  HandleScope handle_scope;
  String::AsciiValue input_string(args[0]->ToString());
  if (!*input_string)
    return Undefined();

  char *cache_string = new char[strlen(*input_string)];
  if (!cache_string)
    return Undefined();
  strcpy(cache_string, *input_string);
  cleanURL(cache_string);
  Handle<String> hString = String::New(cache_string);
  delete[] cache_string;

  return hString;
}

static Handle<Value> ku6_alert(const Arguments &args)
{
  if (args.Length() != 1)
    return Undefined();

  HandleScope handle_scope;
  String::AsciiValue msg(args[0]->ToString());
#if defined(_MSC_VER)
  ::MessageBox(NULL, *msg, "JSDownloader",MB_OK);
#else
  printf("%s\n", *msg);
#endif

  return Undefined();
}

static int ku6_download_files(char *pURL, char sep, char *output_path)
{
  char cmd[8*1024], *p;
  int file_count = 1;

  // Avoid NULL and zero length string.
  if (!pURL || !*pURL) {
    printf("The media url returned by JS is NULL\n");
    return -1;
  }

  do {
    p = strchr(pURL, sep);
    if (p)
      *p = '\0';
#if defined(USE_CURL_API_DOWNLOAD)
    sprintf(cmd, "%s_file_%04d.flv", output_path, file_count++);
    FILE *fp = fopen(cmd, "wb");
    if (!fp) {
      if (p)
        *p = sep;
      break;
    }
    if (curl_download(curl_write_file, fp, pURL, 0, NULL, 1) < 0) {
      *p = sep;
      fclose(fp);
      break;
    }
    fclose(fp);
#else
    sprintf(cmd, "curl --compressed --max-redirs %d -A \"%s\" -L -o %s_file_%04d.flv \"%s\"", MAX_REDIRECT_NUM, g_user_agent, output_path, file_count++, pURL);
    system(cmd);
#endif
    if (p)
      *p++ = sep;
    pURL = p;
  } while(pURL);

  return 0;
}

static int ku6_makeup_file_name(char *output_path, char *input_url)
{
  char *p = output_path + strlen(output_path) - 1;

  // Append the path endline
  if (*p != '/' && *p != '\\') {
    p[1] = '/';
    p[2] = '\0';
  }
  p = strrchr(input_url, '/');
  if (p) {
    p++;
    if (*p) {
      char *pt = strchr(p, '.');
      if (pt) {
        *pt = '\0';
        strcat(output_path, p);
        *pt = '.';
      }
    }
  }

  return 0;
}

int main(int argc, char* argv[])
{
  // MUST initialize
  char input_js[1024+1]      = {'\0'};
  char input_url[4096+1]     = {'\0'};
  char output_path[1024+1]   = {'\0'};

  if (argc != 7) {
    printf("Usage: %s -i input.js -r intput_url -o output_dir\n", argv[0]);
    exit(1);
  }

  for (int i = 1; i < argc-1; i += 2) {
    char *p = argv[i];
    if (*p != '-') {
      printf("Argument must be started by character: \'-\'\n");
      exit(1);
    }
    p++;
    switch(*p) {
      case 'i':
      case 'I':
        strncpy(input_js, argv[i+1], 1024);
        break;
      case 'r':
      case 'R':
        strncpy(input_url, argv[i+1], 4096);
        break;
      case 'o':
      case 'O':
        strncpy(output_path, argv[i+1], 1024);
        break;
      default:
        printf("Unknown argument: %s", p);
        exit(1);
        break;
    }
  }

  // Makeup the unique output file prefix based on |input_url|.
  ku6_makeup_file_name(output_path, input_url);

  // Invoke exactly once for each application that uses libcurl
  // and before any call of other libcurl functions.
  CURLcode ret = curl_global_init(CURL_GLOBAL_ALL);
  if (ret != CURLE_OK) {
    printf("curl_global_init() fails\n");
    exit(1);
  }

  // Create a stack-allocated handle scope.
  HandleScope handle_scope;

  // Create global C++ register template.
  Handle<ObjectTemplate> global_templ = ObjectTemplate::New();

  // Register C++ function to V8.
  global_templ->Set(String::New("download_page"), FunctionTemplate::New(ku6_download_page));
  global_templ->Set(String::New("download_page_post"), FunctionTemplate::New(ku6_download_page_post));
  global_templ->Set(String::New("parse_xml"), FunctionTemplate::New(ku6_parse_xml));
  global_templ->Set(String::New("parse_xml_attribute"), FunctionTemplate::New(ku6_parse_xml_attribute));
  global_templ->Set(String::New("copyBetween"), FunctionTemplate::New(ku6_copyBetween));
  global_templ->Set(String::New("strReplace"), FunctionTemplate::New(ku6_strReplace));
  global_templ->Set(String::New("strFormat"), FunctionTemplate::New(ku6_strFormat));
  global_templ->Set(String::New("clean_url"), FunctionTemplate::New(ku6_clean_url));
  global_templ->Set(String::New("alert"), FunctionTemplate::New(ku6_alert));

  // Create a new context.
  Persistent<Context> context = Context::New(NULL, global_templ);

  // Enter the created context for compiling.
  Context::Scope context_scope(context);

  // Create a string containing the JavaScript source code.
  FILE *fp = fopen(input_js, "r");
  if (!fp) {
    printf("Cann't open file: %s \n", input_js);
    curl_global_cleanup();
    exit(1);
  }
  fseek(fp, 0, SEEK_END);
  int js_len = ftell(fp);
  fseek(fp, 0, SEEK_SET);
  // MUST be +1
  char *pJSData = new char[js_len+1];
  if (!pJSData) {
    printf("Memory allocation fails\n");
    curl_global_cleanup();
    fclose(fp);
    exit(1);
  }
  // NOTE: "\r\n"(0x0d0x10) -->"\n"(0x10) will reduce file size,
  // We MUST zero the mem, terminate string with NULL.
  memset(pJSData, 0, js_len+1);
  fread(pJSData, js_len, 1, fp);
  fclose(fp);

  // Must calc the actual length.
  js_len = strlen(pJSData);
  Handle<String> source = String::New(pJSData, js_len);
  delete[] pJSData;

  // Compile the source code.
  Handle<Script> script = Script::Compile(source);

  // Run the script to get the result.
  Handle<Value> result = script->Run();

  // Invoke JS function in C++.
  Handle<String> js_func_name = String::New("js_get_media_url");;
  Handle<Value>  js_func_val  = context->Global()->Get(js_func_name);;

  if (js_func_val->IsFunction()) {
    Handle<Function> js_func = Handle<Function>::Cast(js_func_val);
    Handle<Value> http_string = String::New(input_url);
    result = js_func->Call(context->Global(), 1, &http_string);
  }
  // Dispose the persistent context.
  context.Dispose();

  // Convert the result to an ASCII string
  String::AsciiValue urls(result);

  // Extract muli-URLs which is seperated by SEP_INDICATOR_CHAR.
  ku6_download_files(*urls, SEP_INDICATOR_CHAR, output_path);

  // Invoke exactly once for each application that uses libcurl.
  curl_global_cleanup();

  return 0;
}
